﻿Imports System.Reflection
Imports System.Text.RegularExpressions

Public NotInheritable Class ChildRelationFactory
    Private Shared _factory As ChildRelationFactory = Nothing

    Public Shared Function Instance() As ChildRelationFactory
        '#97　MySQL対応
        'DBMSごとの生成クラスを統合
        If _factory Is Nothing Then
            _factory = New ChildRelationFactory
        End If
        Return _factory
    End Function

    ''' <summary>
    ''' 子関連属性を生成します
    ''' </summary>
    ''' <param name="atr"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Function Create(ByVal atr As TableAttribute) As List(Of ChildRelation)
        Dim rels As New List(Of ChildRelation)

        For Each item In (From el In atr.ModelType.GetProperties Where PropertyReader.IsChild(el))
            rels.AddRange(CreateMain(item))
        Next

        Return rels
    End Function

    ''' <summary>
    ''' 生成処理メインルーチン
    ''' </summary>
    ''' <param name="prop"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Private Function CreateMain(ByVal prop As PropertyInfo) As IEnumerable(Of ChildRelation)
        '指定属性
        Dim manuals As List(Of ChildRelation) = CreateAsManual(prop)

        '指定属性がある場合、それを返却します
        If manuals IsNot Nothing AndAlso manuals.Any Then Return manuals

        '制約上属性
        Try
            Dim masters As IEnumerable(Of ChildRelation) = CreateAsMaster(prop)
            Return masters
        Catch ex As Exception
            Throw New InvalidOperationException(String.Format("制約上の子関連属性を自動作成できませんでした。（型：{0}、プロパティ：{1}）", prop.DeclaringType.Name, prop.Name), ex)
        End Try
    End Function

    ''' <summary>
    ''' 指定上の属性を生成します
    ''' </summary>
    ''' <param name="prop"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Private Function CreateAsManual(ByVal prop As PropertyInfo) As IEnumerable(Of ChildRelation)
        Dim atrs As New List(Of ChildRelation)

        'リレーション属性でループ
        For Each item As RelationAttribute In (From el In AttributeReader.Read(Of RelationAttribute)(prop))
            Dim yourColumnName = item.YourColumnName
            Dim yourColumn = (From el As ColumnAttribute In ColumnAttributeReader.Read(prop.PropertyType.GetGenericArguments.First) Where el.IsParent AndAlso el.ColumnName = yourColumnName).FirstOrDefault

            Dim atr As ChildRelation = New ChildRelation(prop, yourColumn)

            atrs.Add(atr)
        Next

        Return atrs
    End Function

    ''' <summary>
    ''' 制約上の属性を生成します
    ''' </summary>
    ''' <param name="prop"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Private Function CreateAsMaster(ByVal prop As PropertyInfo) As IEnumerable(Of ChildRelation)
        If prop.DeclaringType.Equals(prop.PropertyType) Then Throw New InvalidOperationException("自己参照の場合、手動で指定する必要があります。")

        Dim atrs As New List(Of ChildRelation)

        '自分の主キーでループ
        For Each item As ColumnAttribute In (From el In ColumnAttributeReader.ReadPrimaryKeys(prop.DeclaringType) Where el.IsLoadKey)
            Dim yourColumn As ColumnAttribute = Me.GetYourColumn(item, prop)

            Dim atr As ChildRelation = New ChildRelation(prop, yourColumn)

            atrs.Add(atr)
        Next

        Return atrs
    End Function

    ''' <summary>
    ''' 相手の列属性を取得します
    ''' </summary>
    ''' <remarks>
    ''' 親から子への参照仕様は
    ''' ・子クラス内にプロパティ型が親クラスのプロパティが1つだけ存在している
    ''' だけです
    ''' ただし、子クラス内にある列名は、
    ''' 　A.「自クラスプロパティ接頭語」＋「親主キー列名」
    '''   B.親クラス非継承時、「親主キー列名」（接頭語なし）
    ''' 　C.「親クラス接頭語」＋「親主キー列名」
    ''' 　D.「親クラス名」＋「親主キー列名」
    ''' の4種類（優先順位順）あるので注意
    ''' </remarks>
    Private Function GetYourColumn(ByVal myColumn As ColumnAttribute, collectionProp As PropertyInfo) As ParentRelationColumnAttribute
        Dim parentType = collectionProp.DeclaringType
        Dim childType As Type = collectionProp.PropertyType.GetGenericArguments.First

        Dim q As IEnumerable(Of ColumnAttribute) = (From el As ColumnAttribute In ColumnAttributeReader.Read(childType) Where el.IsParent AndAlso el.PropertyInfo.PropertyType.Equals(parentType))

#If DEBUG Then
        If q.Any = False Then Throw New InvalidOperationException(String.Format("子クラス内に親クラスを格納するプロパティがありません。（子：{0}、親:{1}）", childType.Name, parentType.Name))
#End If

        '#101 生成されるSQL文にエスケープ文字を追加する
        Dim yourColumnName As String = Me.CreateYourColumnName(q, myColumn.PlainColumnName)
        yourColumnName = ColumnAttribute.OnColumnNameChanging.Invoke(yourColumnName)

        Dim col = q.Where(Function(c) If(c.ColumnName = yourColumnName, True, False)).FirstOrDefault

#If DEBUG Then
        If col Is Nothing Then
            Dim msg As String = String.Format("相手の列属性を取得できませんでした。（型：{0}、列：{1}）", childType.Name, yourColumnName)
            Throw New InvalidOperationException(msg)
        End If
#End If

        Return col
    End Function

    ''' <summary>
    ''' 親と結合できる列名を取得します。
    ''' </summary>
    ''' <param name="childColumns">結合候補列クエリ。</param>
    ''' <param name="myColumnName">親の主キー列名</param>
    ''' <returns></returns>
    ''' <remarks>
    ''' 「親（自分）」から見て「子（相手）」に結合列名を調べるロジック。
    ''' なお取得した列名は「子（相手）」が保持することになります。
    ''' 
    ''' 規約と命名規則
    ''' .プロパティ名は親の型名と同じにすること。ただし、接頭語は認める。
    ''' .列名は親主キー列名と同じであると解釈される。ただし、例外があるので、優先順位順に記載する。
    ''' 　A.　プロパティに接頭語がある場合、列名は「接頭語」＋「親主キー列名」が適用される。
    ''' 　B.　親クラスが継承をしていない場合、列名は「親主キー列名」が適用される。
    ''' 　C.　親クラスが継承している場合、列名は「親クラス名」＋「親主キー列名」が適用される。（#66）
    ''' 　　　ただし、親クラスが接頭語形式で命名されている場合、列名は「親クラス接頭語」＋「親主キー列名」が適用される。（#80）
    ''' </remarks>
    Private Function CreateYourColumnName(childColumns As IEnumerable(Of ColumnAttribute), ByVal myColumnName As String) As String
        Dim getPrefix As Func(Of String) =
            Function()
                'パターンA
                Dim yourProp = childColumns.First.PropertyInfo
                Dim m = Regex.Match(yourProp.Name, String.Format("(?<prefix>^.+){0}$", yourProp.PropertyType.Name))
                If m.Success Then Return m.Groups("prefix").Value

                '継承していない場合、パターンB（接頭語なし）
                Dim yourtTab = yourProp.PropertyType.ToTable
                If yourtTab.IsRootBase Then Return String.Empty

                'パターンC例外
                m = Regex.Match(yourtTab.ModelType.Name, String.Format("(?<prefix>^.+){0}$", yourtTab.ModelType.BaseType.Name))
                If m.Success Then Return m.Groups("prefix").Value

                'パターンC
                Return yourtTab.ModelType.Name
            End Function

        Dim name As String = StringFormConverter.Instance.ToPrefix(getPrefix.Invoke) & myColumnName

        Return StringFormConverter.Instance.ToColumn(name)
    End Function

End Class
